Repository navigation
feat(runtime,client,cli): export a deployment's whole ontology as installable packages in one read (GET /packages/export, os package export) - #29755
Conversation
…kages/export and os package export The runtime dispatcher answers GET /api/v1/packages/export with every installed package whose registry row is not `code`, disabled ones included, each exactly as GET /packages/:id/export answers it, in dependency order. Same read gate as the per-package export, closed empty query set, whole or absent. client.packages.exportAll() calls it, and os package export [--out DIR] writes one numbered directory per package, each installable with os package install. The spec description drops its PLANNED marker. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbWrCtyQtTGt4iYTTvX
…d export bundle Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbWrCtyQtTGt4iYTTvX
… row Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbWrCtyQtTGt4iYTTvX
|
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS Contract-review-claim: session_01JQwxbacSXJQqAztgt4UMrS · head 43a7c55 · 2026-11-03T03:35Z |
|
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS Contract reviewServed-tier: ① Derived judgments
② Semver levelThe changeset ③ Boundary flagsDev report 6594757585, deviations and open questions:
④ Design judgment
No must-fix. One should-fix (the credential pin's strength) and one escalation (③ item 6). Implemented-by: VERDICT: PASS |
…-ahcrjb # Conflicts: # packages/runtime/src/route-ledger.ts
📓 Docs Drift CheckThis PR changes 4 package(s): 67 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 180 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a67144c1e725763b36e5282e20a0e234a464d29c && git checkout a67144c1e725763b36e5282e20a0e234a464d29c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3eeb81d19495b9e4064685fc9662936a834f017c 85be2297550fee1e478fb1b3e18e2f9de085a54c && git checkout -B drift-repro 3eeb81d19495b9e4064685fc9662936a834f017c && git merge --no-ff 85be2297550fee1e478fb1b3e18e2f9de085a54c
node scripts/docs-audit/affected-docs.mjs --json 3eeb81d19495b9e4064685fc9662936a834f017c
|
|
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS 席位核对ACCEPT · Regen-provenance: 6594891654 ·
|
…he reference after merging main With #29742 (nav arm), #29755 (whole-deployment export) and this change (withheldMaps) all served, no member of package-lifecycle.zod.ts is declared ahead of its producer. Claude-Session: https://claude.ai/code/session_01JQwxbYXRMR5S5B7JSmKkV7 Co-authored-by: Claude <noreply@anthropic.com>
Requested by Warren · project thread
Fixes #28914
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
GET /api/v1/packages/export,一次导出整个部署的本体:所有不是由部署自身代码注册的已安装包(市场安装的、库里创作的,含.custom配对包,停用的也算),每一项就是现有单包导出GET /packages/:id/export的原样回答,按依赖顺序排(基础包在前)。配套 SDK 方法client.packages.exportAll()和 CLI 命令os package export [--out DIR](每个包写一个带序号的目录,可直接os package install);spec 去掉[PLANNED — not yet served]标记。PackageExportBundleSchema) #29100 落地,阻塞的 spec + runtime: a package export carries no namespace and no dependencies, so a reinstalled .custom pair lands under the wrong namespace without its base edge; declare both on PackageExportManifestSchema and emit them #29617(导出带 namespace / dependencies)已由 feat(spec,runtime): a package export carries its namespace and dependencies, so a reinstalled .custom pair keeps both (#29617) #29651 落地,本 PR 是最后的服务端切片。studio.access/setup.access)。回滚即撤回本 PR。packages/runtime/src/domains/packages.ts的assemblePackageExportBundle与新路由分支(在GET /packages/:id之前);packages/cli/src/commands/package/export.ts;packages/spec/src/api/package-lifecycle.zod.ts的 docblock 与 describe(去标记的公开面)。pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-export-bundle.test.ts,预期 10 条全绿(选包、依赖序、单一生产者、读门、查询参数拒绝、整体或不答)。What changes
packages/runtime/src/domains/packages.ts). A new branch answersGET /packages/export, matched ahead ofGET /packages/:id(exportis never a package id:MANIFEST_ID_PATTERNneeds two dot-separated segments). It asks the per-package export's read gate first (requireReadCapability:studio.accessorsetup.access, or a system context), then refuses any query parameter400 VALIDATION_ERRORnaming it (unknownQueryParamRefusalMessage(query, []), the same closed-set refusalGET /packages/:id/customizationsuses).assemblePackageExportBundlethen:sourceis notcode, disabled rows included, each id once (the row'ssource, nevermanifest.scopeorwritable, asPackageExportBundleSchemastates);orderHydrationBatchfrom@objectstack/core(ADR-0141 D5), overdeclaredDependencyIds(row.manifest), so a.custompair follows its base whatever order the registry lists them in;assemblePackageManifest, the per-package export's producer, and nothing else: no second clean or redaction rule;errorFromThrown(its status kept) with the message prefixedThe export of package 'ID' could not be assembled, so no bundle is answered:; anullassembly answers 500 naming the package.packages/runtime/src/dispatcher-plugin.ts).mountPackagesRoute('get', '/export', …), registered before/:idso the literal segment wins.packages/runtime/src/route-ledger.ts). RowGET /packages/export→packages.exportAll,responseSchema: 'PackageExportBundleSchema'; the census sentence regenerated (116 → 117) bycheck-route-ledger-census.mjs --fix.packages/client/src/index.ts).client.packages.exportAll(), answering aPackageExportBundle; its row inunwrap-misfire.pin.test.ts's roster (PackageExportBundleSchemadeclarespackagesonly, so thesuccess+dataheuristic cannot fire).packages/cli/src/commands/package/export.ts, exported fromsrc/index.ts).os package export [--out DIR] [--url] [--token], throughcreateApiClientlike theos metacommands. It writesDIR/NN-ID/objectstack.json, one per entry, numbered in the order served (zero-padded so a listing sorts in install order), and prints theos package installcommands in that order. Loud and never partial: a refusal prints with status and code and writes nothing; an entry with no id is refused naming its position; an--outdirectory that already holds files is refused before the read; an empty bundle writes nothing and says why.packages/spec/src/api/package-lifecycle.zod.ts). The[PLANNED — not yet served]marker comes offPackageExportBundleSchema's describe, which now names the route andos package export; the docblock's "MUST do / not served today" prose now states what the door does. No key, type or accept set changes. The test that pinned the marker is flipped (noPLANNED, names the route and the command, no tracker id); the regeneratedcontent/docs/references/api/package-lifecycle.mdxfollows (check:generated --fix).content/docs/deployment/cli.mdx: a row in the package command table and anos package exportsection (what is and is not exported, the numbered install order, no secret, no enabled state, whole or nothing, who can export, options).content/docs/concepts/metadata-lifecycle.mdx: the export paragraph now names the whole-deployment read beside the per-package one..changeset/28914-package-export-whole-deployment.md: runtime, client, cli, spec allpatch(kindnone, a non-protocol widening per AGENTS.md's level rule).Why
The card's server slice: the maintainer's ruling on docs.objectos.ai #334 (record 6568804196) approved building the whole-ontology export; the spec slice (#29100) declared the answer and the blocker #29617 (#29651) made each per-package export carry what a reinstall reads. This PR serves the read and the command and meets the card's two Done tests.
Tests
packages/runtime/src/domains/packages-export-bundle.test.ts, real dispatcher over a registry and protocol double, 10 tests): selection (codeexcluded, disabled included, each once, parses asPackageExportBundleSchema); dependency order with the registry listing both dependents ahead of the base (control asserted); each entrytoEqualtheGET /packages/:id/exportanswer for the same id; empty when every row iscode; the read gate (studio.accessandsetup.accessadmitted,manage_metadataalone 403 with the registry and protocol never read);?format=zip→ 400VALIDATION_ERRORnamingformat, registry never read; whole or absent twice (no protocol → 500 naming the first package; a throw on the second package's assembly → its 503, message naming that package, nodata);exportnever reachesgetPackage; the ledger row.packages/cli/test/package-export.test.ts, the real command and the realObjectStackClient, onlycreateApiClientstubbed, 6 tests): numbered directories with each entry as served and the install commands in order; each written file is themanifestthe realos package install FILEposts, unchanged (install driven per file, its POST captured); a 503 refusal printed with status and code, exit 1, nothing written; a non-empty--outrefused before any request; an empty bundle writes nothing; an id-less entry refused naming its position.packages/qa/dogfood/test/package-export-bundle-round-trip.dogfood.test.ts, two real showcase boots with install-local mounted, 5 tests). Source: a base installed through install-local (object, app, dashboard, flow, permission set, and a connector withauth: { type: 'bearer', credentialRef }), its.custompair with a published dashboard override, and a third package depending on the base and extending its object. The bundle is exactly those three ids (the showcase's owncodepackage absent), the base first, the pair carryingnamespaceexportbase_custom, its base edge and its override; the connector carries itscredentialRefand the secret the reference resolves to on the source (control: set in the process env) appears nowhere in the response bytes. Target: each entry installs 200 throughPOST /marketplace/install-localwith{ manifest: entry }(the bodyos package install FILEsends), in the order served, and the target's own bundle answers the same ids in the same order with the same namespace, dependencies and per-collection item names and counts; the pair's override comes back with itsoverridesblock.packages-lifecycle.test.tspinsexportAll→GET /api/v1/packages/exportand the order served.Ablation (one-time, runtime tests resolve packages.ts from source; scripts/ablation-replace.mjs)
On
699399a4, fix committed first:orderHydrationBatch(nodes).ordered→nodes.map((n) => n.item):Tests 2 failed | 8 passed (10)(dependency order; and the null-protocol case, which names the first package and so reads the order).row?.source === 'code'→row?.source === 'never':Tests 2 failed | 8 passed (10)(selection; empty bundle).blob after restore == blob at HEAD (8a46b0cd25ab),git diff HEADempty.Acceptance notes
errorFromThrownresolves for it; anullone (no metadata protocol) answers 500. No new error code.Clause-②: yes (a new served route)from the claim is written here asyes (widening): AGENTS.md's closed arm set is(widening)/(narrowing), and a new route that changes no existing answer is a widening.Metadata-protocol: none: nothing a metadata author writes changes.assemblePackageManifestswallows a per-type read error as "type unsupported here" (continue), as before; the bundle inherits that. Not changed here.Verification
Head
43a7c550(merge basec9636f1e); every heavy run underos-verify-lock.pnpm --filter @objectstack/spec test:Test Files 803 passed (803),Tests 23792 passed | 1 todo; typecheck exit 0.pnpm --filter @objectstack/client test:Test Files 54 passed (54),Tests 695 passed; typecheck exit 0.pnpm --filter @objectstack/cli exec vitest run --project unit:Test Files 376 passed (376),Tests 5295 passed; typecheck exit 0. Theintegrationtier is CI's (the diff touches no integration-tier file or spawn entry).pnpm --filter @objectstack/runtime test:Test Files 532 passed (532),Tests 8535 passed | 19 skipped; typecheck exit 0.pnpm --filter @objectstack/dogfood typecheckexit 0; the round-trip fileTests 5 passed (5)after building the dogfood closure.pnpm --filter @objectstack/spec check:generated --fix: 1 stale (content/docs/references/**), regenerated and re-checked green.699399a4;43a7c550changes one comment digit inroute-ledger.ts(the census sentence).dispatch-gates --commands and the ran list
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 111 derived, the same list on699399a4and43a7c550. First pass on699399a4: 107 exit 0;check:route-ledger-censusexit 1 (the census sentence read 116 against 117 rows: fixed with its own--fixin43a7c550); exit 3 (prerequisite not met) forcheck:skill-examples(client-react unbuilt) andcheck:dual-build-cjs-loads(seven packages withoutdist/), both exit 0 on43a7c550after building those packages;check-plugin-teardown-shape.mjs --self-testexit 3 on both heads (its fixture commit is outside the shallow clone): NOT MEASURED.Re-run on
43a7c550, exit 0 each:check:route-ledger-census,check:skill-examples,check:dual-build-cjs-loads,check:type-check-debt,check:query-options-erasure,check:route-envelope,check:dispatcher-error-vocabulary,check:system-context-census,check:spec-docblock-symbol-anchors,check:nul-bytes, and repo-widepnpm lint(eslint . --no-inline-config).dispatch-gates --ran: "111 derived famil(ies) accounted for — 110 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0. CI runs every family.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses (exit 0 on43a7c550).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code